/**
 * The contents of this file are subject to the license and copyright detailed
 * in the LICENSE and NOTICE files at the root of the source tree and available
 * online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.content;

import java.io.IOException;
import java.io.InputStream;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.dspace.app.util.AuthorizeUtil;
import org.dspace.authorize.AuthorizeConfiguration;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.AuthorizeManager;
import org.dspace.authorize.ResourcePolicy;
import org.dspace.browse.BrowseException;
import org.dspace.browse.IndexBrowse;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.core.LogManager;
import org.dspace.content.authority.Choices;
import org.dspace.content.authority.ChoiceAuthorityManager;
import org.dspace.content.authority.MetadataAuthorityManager;
import org.dspace.core.ConfigurationManager;
import org.dspace.event.Event;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.handle.HandleManager;
import org.dspace.storage.rdbms.DatabaseManager;
import org.dspace.storage.rdbms.TableRow;
import org.dspace.storage.rdbms.TableRowIterator;
import proj.oceandocs.citation.CitationManager;

/**
 * Class representing an item in DSpace. <P> This class holds in memory the item
 * Dublin Core metadata, the bundles in the item, and the bitstreams in those
 * bundles. When modifying the item, if you modify the Dublin Core or the "in
 * archive" flag, you must call
 * <code>update</code> for the changes to be written to the database. Creating,
 * adding or removing bundles or bitstreams has immediate effect in the
 * database.
 *
 * @author Robert Tansley
 * @author Martin Hald
 * @version $Revision: 6107 $
 */
public class Item extends DSpaceObject {

  /**
   * Wild card for Dublin Core metadata qualifiers/languages
   */
  public static final String ANY = "*";
  /**
   * log4j category
   */
  private static final Logger log = Logger.getLogger(Item.class);
  /**
   * Our context
   */
  private Context ourContext;
  /**
   * The table row corresponding to this item
   */
  private TableRow itemRow;
  /**
   * The e-person who submitted this item
   */
  private EPerson submitter;
  /**
   * The bundles in this item - kept in sync with DB
   */
  private List<Bundle> bundles;
  /**
   * The Dublin Core metadata - inner class for lazy loading
   */
  MetadataCache dublinCore = new MetadataCache();
  /**
   * Handle, if any
   */
  private String handle;
  /**
   * True if the Dublin Core has changed since reading from the DB or the last
   * update()
   */
  private boolean dublinCoreChanged;
  /**
   * True if anything else was changed since last update() (to drive event
   * mechanism)
   */
  private boolean modified;

  /**
   * Construct an item with the given table row
   *
   * @param context the context this object exists in
   * @param row the corresponding row in the table
   * @throws SQLException
   */
  Item(Context context, TableRow row) throws SQLException {
    ourContext = context;
    itemRow = row;
    dublinCoreChanged = false;
    modified = false;
    clearDetails();

    // Get our Handle if any
    handle = HandleManager.findHandle(context, this);

    // Cache ourselves
    context.cache(this, row.getIntColumn("item_id"));
  }

  private TableRowIterator retrieveMetadata() throws SQLException {
    return DatabaseManager.queryTable(ourContext, "MetadataValue",
            "SELECT * FROM MetadataValue WHERE item_id= ? ORDER BY metadata_field_id, place",
            itemRow.getIntColumn("item_id"));
  }

  /**
   * Get an item from the database. The item, its Dublin Core metadata, and the
   * bundle and bitstream metadata are all loaded into memory.
   *
   * @param context DSpace context object
   * @param id Internal ID of the item
   * @return the item, or null if the internal ID is invalid.
   * @throws SQLException
   */
  public static Item find(Context context, int id) throws SQLException {
    // First check the cache
    Item fromCache = (Item) context.fromCache(Item.class, id);

    if (fromCache != null) {
      return fromCache;
    }

    TableRow row = DatabaseManager.find(context, "item", id);

    if (row == null) {
      if (log.isDebugEnabled()) {
        log.debug(LogManager.getHeader(context, "find_item",
                "not_found,item_id=" + id));
      }

      return null;
    }

    // not null, return item
    if (log.isDebugEnabled()) {
      log.debug(LogManager.getHeader(context, "find_item", "item_id="
              + id));
    }

    return new Item(context, row);
  }

  /**
   * Create a new item, with a new internal ID. This method is not public, since
   * items need to be created as workspace items. Authorisation is the
   * responsibility of the caller.
   *
   * @param context DSpace context object
   * @return the newly created item
   * @throws SQLException
   * @throws AuthorizeException
   */
  public static Item create(Context context) throws SQLException, AuthorizeException {
    TableRow row = DatabaseManager.create(context, "item");
    Item i = new Item(context, row);

    // Call update to give the item a last modified date. OK this isn't
    // amazingly efficient but creates don't happen that often.
    context.turnOffAuthorisationSystem();
    i.update();
    context.restoreAuthSystemState();

    context.addEvent(new Event(Event.CREATE, Constants.ITEM, i.getID(), null));

    log.info(LogManager.getHeader(context, "create_item", "item_id="
            + row.getIntColumn("item_id")));

    return i;
  }

  /**
   * Get all the items in the archive. Only items with the "in archive" flag set
   * are included. The order of the list is indeterminate.
   *
   * @param context DSpace context object
   * @return an iterator over the items in the archive.
   * @throws SQLException
   */
  public static ItemIterator findAll(Context context) throws SQLException {
    String myQuery = "SELECT * FROM item WHERE in_archive='1'";

    TableRowIterator rows = DatabaseManager.queryTable(context, "item", myQuery);

    return new ItemIterator(context, rows);
  }

  /**
   * Find all the items in the archive by a given submitter. The order is
   * indeterminate. Only items with the "in archive" flag set are included.
   *
   * @param context DSpace context object
   * @param eperson the submitter
   * @return an iterator over the items submitted by eperson
   * @throws SQLException
   */
  public static ItemIterator findBySubmitter(Context context, EPerson eperson)
          throws SQLException {
    String myQuery = "SELECT * FROM item WHERE in_archive='1' AND submitter_id="
            + eperson.getID();

    TableRowIterator rows = DatabaseManager.queryTable(context, "item", myQuery);

    return new ItemIterator(context, rows);
  }

  /**
   * Get the internal ID of this item. In general, this shouldn't be exposed to
   * users
   *
   * @return the internal identifier
   */
  public int getID() {
    return itemRow.getIntColumn("item_id");
  }

  /**
   * @see org.dspace.content.DSpaceObject#getHandle()
   */
  public String getHandle() {
    if (handle == null) {
      try {
        handle = HandleManager.findHandle(this.ourContext, this);
      } catch (SQLException e) {
        // TODO Auto-generated catch block
        //e.printStackTrace();
      }
    }
    return handle;
  }

  /**
   * Find out if the item is part of the main archive
   *
   * @return true if the item is in the main archive
   */
  public boolean isArchived() {
    return itemRow.getBooleanColumn("in_archive");
  }

  /**
   * Find out if the item has been withdrawn
   *
   * @return true if the item has been withdrawn
   */
  public boolean isWithdrawn() {
    return itemRow.getBooleanColumn("withdrawn");
  }

  /**
   * Get the date the item was last modified, or the current date if
   * last_modified is null
   *
   * @return the date the item was last modified, or the current date if the
   * column is null.
   */
  public Date getLastModified() {
    Date myDate = itemRow.getDateColumn("last_modified");

    if (myDate == null) {
      myDate = new Date();
    }

    return myDate;
  }

  /**
   * Set the "is_archived" flag. This is public and only
   * <code>WorkflowItem.archive()</code> should set this.
   *
   * @param isArchived new value for the flag
   */
  public void setArchived(boolean isArchived) {
    itemRow.setColumn("in_archive", isArchived);
    modified = true;
  }

  /**
   * Set the owning Collection for the item
   *
   * @param c Collection
   */
  public void setOwningCollection(Collection c) {
    itemRow.setColumn("owning_collection", c.getID());
    modified = true;
  }

  /**
   * Get the owning Collection for the item
   *
   * @return Collection that is the owner of the item
   * @throws SQLException
   */
  public Collection getOwningCollection() throws java.sql.SQLException {
    Collection myCollection = null;

    // get the collection ID
    int cid = itemRow.getIntColumn("owning_collection");

    myCollection = Collection.find(ourContext, cid);

    return myCollection;
  }

  // just get the collection ID for internal use
  private int getOwningCollectionID() {
    return itemRow.getIntColumn("owning_collection");
  }

  /**
   * Get Dublin Core metadata for the item. Passing in a
   * <code>null</code> value for
   * <code>qualifier</code> or
   * <code>lang</code> only matches Dublin Core fields where that qualifier or
   * languages is actually
   * <code>null</code>. Passing in
   * <code>Item.ANY</code> retrieves all metadata fields with any value for the
   * qualifier or language, including
   * <code>null</code> <P> Examples: <P> Return values of the unqualified
   * "title" field, in any language. Qualified title fields (e.g.
   * "title.uniform") are NOT returned: <P>
   * <code>item.getDC( "title", null, Item.ANY );</code> <P> Return all US
   * English values of the "title" element, with any qualifier (including
   * unqualified): <P>
   * <code>item.getDC( "title", Item.ANY, "en_US" );</code> <P> The ordering of
   * values of a particular element/qualifier/language combination is
   * significant. When retrieving with wildcards, values of a particular
   * element/qualifier/language combinations will be adjacent, but the overall
   * ordering of the combinations is indeterminate.
   *
   * @param element the Dublin Core element. <code>Item.ANY</code> matches any
   * element. <code>null</code> doesn't really make sense as all DC must have an
   * element.
   * @param qualifier the qualifier. <code>null</code> means unqualified, and
   * <code>Item.ANY</code> means any qualifier (including unqualified.)
   * @param lang the ISO639 language code, optionally followed by an underscore
   * and the ISO3166 country code. <code>null</code> means only values with no
   * language are returned, and <code>Item.ANY</code> means values with any
   * country code or no country code are returned.
   * @return Dublin Core fields that match the parameters
   */
  @Deprecated
  public DCValue[] getDC(String element, String qualifier, String lang) {
    return getMetadata(MetadataSchema.DC_SCHEMA, element, qualifier, lang);
  }

  /**
   * Get metadata for the item in a chosen schema. See
   * <code>MetadataSchema</code> for more information about schemas. Passing in
   * a
   * <code>null</code> value for
   * <code>qualifier</code> or
   * <code>lang</code> only matches metadata fields where that qualifier or
   * languages is actually
   * <code>null</code>. Passing in
   * <code>Item.ANY</code> retrieves all metadata fields with any value for the
   * qualifier or language, including
   * <code>null</code> <P> Examples: <P> Return values of the unqualified
   * "title" field, in any language. Qualified title fields (e.g.
   * "title.uniform") are NOT returned: <P>
   * <code>item.getMetadata("dc", "title", null, Item.ANY );</code> <P> Return
   * all US English values of the "title" element, with any qualifier (including
   * unqualified): <P>
   * <code>item.getMetadata("dc, "title", Item.ANY, "en_US" );</code> <P> The
   * ordering of values of a particular element/qualifier/language combination
   * is significant. When retrieving with wildcards, values of a particular
   * element/qualifier/language combinations will be adjacent, but the overall
   * ordering of the combinations is indeterminate.
   *
   * @param schema the schema for the metadata field. <em>Must</em> match the
   * <code>name</code> of an existing metadata schema.
   * @param element the element name. <code>Item.ANY</code> matches any element.
   * <code>null</code> doesn't really make sense as all metadata must have an
   * element.
   * @param qualifier the qualifier. <code>null</code> means unqualified, and
   * <code>Item.ANY</code> means any qualifier (including unqualified.)
   * @param lang the ISO639 language code, optionally followed by an underscore
   * and the ISO3166 country code. <code>null</code> means only values with no
   * language are returned, and <code>Item.ANY</code> means values with any
   * country code or no country code are returned.
   * @return metadata fields that match the parameters
   */
  public DCValue[] getMetadata(String schema, String element, String qualifier,
          String lang) {
    // Build up list of matching values
    List<DCValue> values = new ArrayList<DCValue>();
    for (DCValue dcv : getMetadata()) {
      if (match(schema, element, qualifier, lang, dcv)) {
        // We will return a copy of the object in case it is altered
        DCValue copy = new DCValue();
        copy.element = dcv.element;
        copy.qualifier = dcv.qualifier;
        copy.value = dcv.value;
        copy.language = dcv.language;
        copy.schema = dcv.schema;
        copy.authority = dcv.authority;
        copy.confidence = dcv.confidence;
        values.add(copy);
      }
    }

    // Create an array of matching values
    DCValue[] valueArray = new DCValue[values.size()];
    valueArray = (DCValue[]) values.toArray(valueArray);

    return valueArray;
  }

  /**
   * Retrieve metadata field values from a given metadata string of the form
   * <schema prefix>.<element>[.<qualifier>|.*]
   *
   * @param mdString The metadata string of the form <schema
   * prefix>.<element>[.<qualifier>|.*]
   */
  public DCValue[] getMetadata(String mdString) {
    StringTokenizer dcf = new StringTokenizer(mdString, ".");

    String[] tokens = {
      "", "", ""
    };
    int i = 0;
    while (dcf.hasMoreTokens()) {
      tokens[i] = dcf.nextToken().trim();//.toLowerCase().trim();
      i++;
    }
    String schema = tokens[0];
    String element = tokens[1];
    String qualifier = tokens[2];

    DCValue[] values;
    if ("*".equals(qualifier)) {
      values = getMetadata(schema, element, Item.ANY, Item.ANY);
    } else if ("".equals(qualifier)) {
      values = getMetadata(schema, element, null, Item.ANY);
    } else {
      values = getMetadata(schema, element, qualifier, Item.ANY);
    }

    return values;
  }

  /**
   * Add Dublin Core metadata fields. These are appended to existing values. Use
   * <code>clearDC</code> to remove values. The ordering of values passed in is
   * maintained.
   *
   * @param element the Dublin Core element
   * @param qualifier the Dublin Core qualifier, or <code>null</code> for
   * unqualified
   * @param lang the ISO639 language code, optionally followed by an underscore
   * and the ISO3166 country code. <code>null</code> means the value has no
   * language (for example, a date).
   * @param values the values to add.
   */
  @Deprecated
  public void addDC(String element, String qualifier, String lang,
          String[] values) {
    addMetadata(MetadataSchema.DC_SCHEMA, element, qualifier, lang, values);
  }

  /**
   * Add a single Dublin Core metadata field. This is appended to existing
   * values. Use
   * <code>clearDC</code> to remove values.
   *
   * @param element the Dublin Core element
   * @param qualifier the Dublin Core qualifier, or <code>null</code> for
   * unqualified
   * @param lang the ISO639 language code, optionally followed by an underscore
   * and the ISO3166 country code. <code>null</code> means the value has no
   * language (for example, a date).
   * @param value the value to add.
   */
  @Deprecated
  public void addDC(String element, String qualifier, String lang,
          String value) {
    addMetadata(MetadataSchema.DC_SCHEMA, element, qualifier, lang, value);
  }

  /**
   * Add metadata fields. These are appended to existing values. Use
   * <code>clearDC</code> to remove values. The ordering of values passed in is
   * maintained. <p> If metadata authority control is available, try to get
   * authority values. The authority confidence depends on whether authority is
   * <em>required</em> or not.
   *
   * @param schema the schema for the metadata field. <em>Must</em> match the
   * <code>name</code> of an existing metadata schema.
   * @param element the metadata element name
   * @param qualifier the metadata qualifier name, or <code>null</code> for
   * unqualified
   * @param lang the ISO639 language code, optionally followed by an underscore
   * and the ISO3166 country code. <code>null</code> means the value has no
   * language (for example, a date).
   * @param values the values to add.
   */
  public void addMetadata(String schema, String element, String qualifier, String lang,
          String[] values) {
    MetadataAuthorityManager mam = MetadataAuthorityManager.getManager();
    String fieldKey = MetadataAuthorityManager.makeFieldKey(schema, element, qualifier);
    if (mam.isAuthorityControlled(fieldKey)) {
      String authorities[] = new String[values.length];
      int confidences[] = new int[values.length];
      for (int i = 0; i < values.length; ++i) {
        Choices c = ChoiceAuthorityManager.getManager().getBestMatch(fieldKey, values[i], getOwningCollectionID(), null);
        authorities[i] = c.values.length > 0 ? c.values[0].authority : null;
        confidences[i] = c.confidence;
      }
      addMetadata(schema, element, qualifier, lang, values, authorities, confidences);
    } else {
      addMetadata(schema, element, qualifier, lang, values, null, null);
    }
  }

  public void addMetadata(String schema, String element, String qualifier, String lang,
          String[] values, boolean authorityControlled) {
    String fieldKey = MetadataField.formKey(schema, element, qualifier);
    if (authorityControlled) {
      String authorities[] = new String[values.length];
      int confidences[] = new int[values.length];
      for (int i = 0; i < values.length; ++i) {
        Choices c = ChoiceAuthorityManager.getManager().getBestMatch(fieldKey, values[i], getOwningCollectionID(), null);
        authorities[i] = c.values.length > 0 ? c.values[0].authority : null;
        confidences[i] = c.confidence;
      }
      addMetadata(schema, element, qualifier, lang, values, authorities, confidences, authorityControlled);
    } else {
      addMetadata(schema, element, qualifier, lang, values, null, null, false);
    }
  }

  /**
   * Add metadata fields. These are appended to existing values. Use
   * <code>clearDC</code> to remove values. The ordering of values passed in is
   * maintained.
   *
   * @param schema the schema for the metadata field. <em>Must</em> match the
   * <code>name</code> of an existing metadata schema.
   * @param element the metadata element name
   * @param qualifier the metadata qualifier name, or <code>null</code> for
   * unqualified
   * @param lang the ISO639 language code, optionally followed by an underscore
   * and the ISO3166 country code. <code>null</code> means the value has no
   * language (for example, a date).
   * @param values the values to add.
   * @param authorities the external authority key for this value (or null)
   * @param confidences the authority confidence (default 0)
   */
  public void addMetadata(String schema, String element, String qualifier, String lang,
          String[] values, String authorities[], int confidences[]) {
    List<DCValue> dublinCore = getMetadata();
    MetadataAuthorityManager mam = MetadataAuthorityManager.getManager();
    boolean authorityControlled = mam.isAuthorityControlled(schema, element, qualifier);
    boolean authorityRequired = mam.isAuthorityRequired(schema, element, qualifier);
    String fieldName = schema + "." + element + ((qualifier == null) ? "" : "." + qualifier);

    // We will not verify that they are valid entries in the registry
    // until update() is called.
    for (int i = 0; i < values.length; i++) {
      DCValue dcv = new DCValue();
      dcv.schema = schema;
      dcv.element = element;
      dcv.qualifier = qualifier;
      dcv.language = (lang == null ? null : lang.trim());

      // Logic to set Authority and Confidence:
      //  - normalize an empty string for authority to NULL.
      //  - if authority key is present, use given confidence or NOVALUE if not given
      //  - otherwise, preserve confidence if meaningful value was given since it may document a failed authority lookup
      //  - CF_UNSET signifies no authority nor meaningful confidence.
      //  - it's possible to have empty authority & CF_ACCEPTED if e.g. user deletes authority key
      if (authorityControlled) {
        if (authorities != null && authorities[i] != null && authorities[i].length() > 0) {
          dcv.authority = authorities[i];
          dcv.confidence = confidences == null ? Choices.CF_NOVALUE : confidences[i];
        } else {
          dcv.authority = null;
          dcv.confidence = confidences == null ? Choices.CF_UNSET : confidences[i];
        }
        // authority sanity check: if authority is required, was it supplied?
        // XXX FIXME? can't throw a "real" exception here without changing all the callers to expect it, so use a runtime exception
        if (authorityRequired && (dcv.authority == null || dcv.authority.length() == 0)) {
          throw new IllegalArgumentException("The metadata field \"" + fieldName + "\" requires an authority key but none was provided. Vaue=\"" + dcv.value + "\"");
        }
      }
      if (values[i] != null) {
        // remove control unicode char
        String temp = values[i].trim();
        char[] dcvalue = temp.toCharArray();
        for (int charPos = 0; charPos < dcvalue.length; charPos++) {
          if (Character.isISOControl(dcvalue[charPos])
                  && !String.valueOf(dcvalue[charPos]).equals("\u0009")
                  && !String.valueOf(dcvalue[charPos]).equals("\n")
                  && !String.valueOf(dcvalue[charPos]).equals("\r")) {
            dcvalue[charPos] = ' ';
          }
        }
        dcv.value = String.valueOf(dcvalue);
      } else {
        dcv.value = null;
      }
      dublinCore.add(dcv);
      addDetails(fieldName);
    }

    if (values.length > 0) {
      dublinCoreChanged = true;
    }
  }

  public void addMetadata(String schema, String element, String qualifier, String lang,
          String[] values, String authorities[], int confidences[], boolean authorityControlled) {
    List<DCValue> dublinCore = getMetadata();
    String fieldName = schema + "." + element + ((qualifier == null) ? "" : "." + qualifier);

    // We will not verify that they are valid entries in the registry
    // until update() is called.
    for (int i = 0; i < values.length; i++) {
      DCValue dcv = new DCValue();
      dcv.schema = schema;
      dcv.element = element;
      dcv.qualifier = qualifier;
      dcv.language = (lang == null ? null : lang.trim());

      // Logic to set Authority and Confidence:
      //  - normalize an empty string for authority to NULL.
      //  - if authority key is present, use given confidence or NOVALUE if not given
      //  - otherwise, preserve confidence if meaningful value was given since it may document a failed authority lookup
      //  - CF_UNSET signifies no authority nor meaningful confidence.
      //  - it's possible to have empty authority & CF_ACCEPTED if e.g. user deletes authority key
      if (authorityControlled) {
        if (authorities != null && authorities[i] != null && authorities[i].length() > 0) {
          dcv.authority = authorities[i];
          dcv.confidence = confidences == null ? Choices.CF_NOVALUE : confidences[i];
        } else {
          dcv.authority = "";
          dcv.confidence = confidences == null ? Choices.CF_UNSET : confidences[i];
        }
        // authority sanity check: if authority is required, was it supplied?
        // XXX FIXME? can't throw a "real" exception here without changing all the callers to expect it, so use a runtime exception
//                if (authorityRequired && (dcv.authority == null || dcv.authority.length() == 0))
//                {
//                    throw new IllegalArgumentException("The metadata field \"" + fieldName + "\" requires an authority key but none was provided. Vaue=\"" + dcv.value + "\"");
//                }
      }
      if (values[i] != null) {
        // remove control unicode char
        String temp = values[i].trim();
        char[] dcvalue = temp.toCharArray();
        for (int charPos = 0; charPos < dcvalue.length; charPos++) {
          if (Character.isISOControl(dcvalue[charPos])
                  && !String.valueOf(dcvalue[charPos]).equals("\u0009")
                  && !String.valueOf(dcvalue[charPos]).equals("\n")
                  && !String.valueOf(dcvalue[charPos]).equals("\r")) {
            dcvalue[charPos] = ' ';
          }
        }
        dcv.value = String.valueOf(dcvalue);
      } else {
        dcv.value = null;
      }
      dublinCore.add(dcv);
      addDetails(fieldName);
    }

    if (values.length > 0) {
      dublinCoreChanged = true;
    }
  }

  /**
   * Add a single metadata field. This is appended to existing values. Use
   * <code>clearDC</code> to remove values.
   *
   * @param schema the schema for the metadata field. <em>Must</em> match the
   * <code>name</code> of an existing metadata schema.
   * @param element the metadata element name
   * @param qualifier the metadata qualifier, or <code>null</code> for
   * unqualified
   * @param lang the ISO639 language code, optionally followed by an underscore
   * and the ISO3166 country code. <code>null</code> means the value has no
   * language (for example, a date).
   * @param value the value to add.
   */
  public void addMetadata(String schema, String element, String qualifier,
          String lang, String value) {
    String[] valArray = new String[1];
    valArray[0] = value;

    addMetadata(schema, element, qualifier, lang, valArray);
  }

  /**
   * Add a single metadata field. This is appended to existing values. Use
   * <code>clearDC</code> to remove values.
   *
   * @param schema the schema for the metadata field. <em>Must</em> match the
   * <code>name</code> of an existing metadata schema.
   * @param element the metadata element name
   * @param qualifier the metadata qualifier, or <code>null</code> for
   * unqualified
   * @param lang the ISO639 language code, optionally followed by an underscore
   * and the ISO3166 country code. <code>null</code> means the value has no
   * language (for example, a date).
   * @param value the value to add.
   * @param authority the external authority key for this value (or null)
   * @param confidence the authority confidence (default 0)
   */
  public void addMetadata(String schema, String element, String qualifier,
          String lang, String value, String authority, int confidence) {
    String[] valArray = new String[1];
    String[] authArray = new String[1];
    int[] confArray = new int[1];
    valArray[0] = value;
    authArray[0] = authority;
    confArray[0] = confidence;

    addMetadata(schema, element, qualifier, lang, valArray, authArray, confArray);
  }

  public void addMetadata(String schema, String element, String qualifier,
          String lang, String value, String authority, int confidence, boolean authorityControlled) {
    String[] valArray = new String[1];
    String[] authArray = new String[1];
    int[] confArray = new int[1];
    valArray[0] = value;
    authArray[0] = authority;
    confArray[0] = confidence;

    addMetadata(schema, element, qualifier, lang, valArray, authArray, confArray, authorityControlled);
  }

  /**
   * Clear Dublin Core metadata values. As with
   * <code>getDC</code> above, passing in
   * <code>null</code> only matches fields where the qualifier or language is
   * actually
   * <code>null</code>.<code>Item.ANY</code> will match any element, qualifier
   * or language, including
   * <code>null</code>. Thus,
   * <code>item.clearDC(Item.ANY, Item.ANY, Item.ANY)</code> will remove all
   * Dublin Core metadata associated with an item.
   *
   * @param element the Dublin Core element to remove, or <code>Item.ANY</code>
   * @param qualifier the qualifier. <code>null</code> means unqualified, and
   * <code>Item.ANY</code> means any qualifier (including unqualified.)
   * @param lang the ISO639 language code, optionally followed by an underscore
   * and the ISO3166 country code. <code>null</code> means only values with no
   * language are removed, and <code>Item.ANY</code> means values with any
   * country code or no country code are removed.
   */
  @Deprecated
  public void clearDC(String element, String qualifier, String lang) {
    clearMetadata(MetadataSchema.DC_SCHEMA, element, qualifier, lang);
  }

  /**
   * Clear metadata values. As with
   * <code>getDC</code> above, passing in
   * <code>null</code> only matches fields where the qualifier or language is
   * actually
   * <code>null</code>.<code>Item.ANY</code> will match any element, qualifier
   * or language, including
   * <code>null</code>. Thus,
   * <code>item.clearDC(Item.ANY, Item.ANY, Item.ANY)</code> will remove all
   * Dublin Core metadata associated with an item.
   *
   * @param schema the schema for the metadata field. <em>Must</em> match the
   * <code>name</code> of an existing metadata schema.
   * @param element the Dublin Core element to remove, or <code>Item.ANY</code>
   * @param qualifier the qualifier. <code>null</code> means unqualified, and
   * <code>Item.ANY</code> means any qualifier (including unqualified.)
   * @param lang the ISO639 language code, optionally followed by an underscore
   * and the ISO3166 country code. <code>null</code> means only values with no
   * language are removed, and <code>Item.ANY</code> means values with any
   * country code or no country code are removed.
   */
  public void clearMetadata(String schema, String element, String qualifier,
          String lang) {
    // We will build a list of values NOT matching the values to clear
    List<DCValue> values = new ArrayList<DCValue>();
    for (DCValue dcv : getMetadata()) {
      if (!match(schema, element, qualifier, lang, dcv)) {
        values.add(dcv);
      }
    }

    // Now swap the old list of values for the new, unremoved values
    setMetadata(values);
    dublinCoreChanged = true;
  }

  /**
   * Utility method for pattern-matching metadata elements. This method will
   * return
   * <code>true</code> if the given schema, element, qualifier and language
   * match the schema, element, qualifier and language of the
   * <code>DCValue</code> object passed in. Any or all of the element, qualifier
   * and language passed in can be the
   * <code>Item.ANY</code> wildcard.
   *
   * @param schema the schema for the metadata field. <em>Must</em> match the
   * <code>name</code> of an existing metadata schema.
   * @param element the element to match, or <code>Item.ANY</code>
   * @param qualifier the qualifier to match, or <code>Item.ANY</code>
   * @param language the language to match, or <code>Item.ANY</code>
   * @param dcv the Dublin Core value
   * @return <code>true</code> if there is a match
   */
  private boolean match(String schema, String element, String qualifier,
          String language, DCValue dcv) {
    // We will attempt to disprove a match - if we can't we have a match
    if (!element.equals(Item.ANY) && !element.equalsIgnoreCase(dcv.element)) {
      // Elements do not match, no wildcard
      return false;
    }

    if (qualifier == null) {
      // Value must be unqualified
      if (dcv.qualifier != null) {
        // Value is qualified, so no match
        return false;
      }
    } else if (!qualifier.equals(Item.ANY)) {
      // Not a wildcard, so qualifier must match exactly
      if (!qualifier.equalsIgnoreCase(dcv.qualifier)) {
        return false;
      }
    }

    if (language == null || language.equals("")) {
      // Value must be null language to match
      if (dcv.language != null || dcv.language.equals("")) {
        // Value is qualified, so no match
        return false;
      }
    } else if (!language.equals(Item.ANY)) {
      // Not a wildcard, so language must match exactly
      if (!language.equalsIgnoreCase(dcv.language)) {
        return false;
      }
    }

    if (!schema.equals(Item.ANY)) {
      if (dcv.schema != null && !dcv.schema.equalsIgnoreCase(schema)) {
        // The namespace doesn't match
        return false;
      }
    }

    // If we get this far, we have a match
    return true;
  }

  /**
   * Get the e-person that originally submitted this item
   *
   * @return the submitter
   */
  public EPerson getSubmitter() throws SQLException {
    if (submitter == null && !itemRow.isColumnNull("submitter_id")) {
      submitter = EPerson.find(ourContext, itemRow.getIntColumn("submitter_id"));
    }
    return submitter;
  }

  /**
   * Set the e-person that originally submitted this item. This is a public
   * method since it is handled by the WorkspaceItem class in the ingest
   * package.
   * <code>update</code> must be called to write the change to the database.
   *
   * @param sub the submitter
   */
  public void setSubmitter(EPerson sub) {
    submitter = sub;

    if (submitter != null) {
      itemRow.setColumn("submitter_id", submitter.getID());
    } else {
      itemRow.setColumnNull("submitter_id");
    }
    modified = true;
  }

  /**
   * See whether this Item is contained by a given Collection.
   *
   * @param collection
   * @return true if {@code collection} contains this Item.
   * @throws SQLException
   */
  public boolean isIn(Collection collection) throws SQLException {
    TableRow tr = DatabaseManager.querySingle(ourContext,
            "SELECT COUNT(*) AS count"
            + " FROM collection2item"
            + " WHERE collection_id = ? AND item_id = ?",
            collection.getID(), itemRow.getIntColumn("item_id"));
    return tr.getLongColumn("count") > 0;
  }

  /**
   * Get the collections this item is in. The order is indeterminate.
   *
   * @return the collections this item is in, if any.
   * @throws SQLException
   */
  public Collection[] getCollections() throws SQLException {
    List<Collection> collections = new ArrayList<Collection>();

    // Get collection table rows
    TableRowIterator tri = DatabaseManager.queryTable(ourContext, "collection",
            "SELECT collection.* FROM collection, collection2item WHERE "
            + "collection2item.collection_id=collection.collection_id AND "
            + "collection2item.item_id= ? ",
            itemRow.getIntColumn("item_id"));

    try {
      while (tri.hasNext()) {
        TableRow row = tri.next();

        // First check the cache
        Collection fromCache = (Collection) ourContext.fromCache(
                Collection.class, row.getIntColumn("collection_id"));

        if (fromCache != null) {
          collections.add(fromCache);
        } else {
          collections.add(new Collection(ourContext, row));
        }
      }
    } finally {
      // close the TableRowIterator to free up resources
      if (tri != null) {
        tri.close();
      }
    }

    Collection[] collectionArray = new Collection[collections.size()];
    collectionArray = (Collection[]) collections.toArray(collectionArray);

    return collectionArray;
  }

  /**
   * Get the communities this item is in. Returns an unordered array of the
   * communities that house the collections this item is in, including parent
   * communities of the owning collections.
   *
   * @return the communities this item is in.
   * @throws SQLException
   */
  public Community[] getCommunities() throws SQLException {
    List<Community> communities = new ArrayList<Community>();

    // Get community table rows
    TableRowIterator tri = DatabaseManager.queryTable(ourContext, "community",
            "SELECT community.* FROM community, community2item "
            + "WHERE community2item.community_id=community.community_id "
            + "AND community2item.item_id= ? ",
            itemRow.getIntColumn("item_id"));

    try {
      while (tri.hasNext()) {
        TableRow row = tri.next();

        // First check the cache
        Community owner = (Community) ourContext.fromCache(Community.class,
                row.getIntColumn("community_id"));

        if (owner == null) {
          owner = new Community(ourContext, row);
        }

        communities.add(owner);

        // now add any parent communities
        Community[] parents = owner.getAllParents();
        communities.addAll(Arrays.asList(parents));
      }
    } finally {
      // close the TableRowIterator to free up resources
      if (tri != null) {
        tri.close();
      }
    }

    Community[] communityArray = new Community[communities.size()];
    communityArray = (Community[]) communities.toArray(communityArray);

    return communityArray;
  }

  /**
   * Get the bundles in this item.
   *
   * @return the bundles in an unordered array
   */
  public Bundle[] getBundles() throws SQLException {
    if (bundles == null) {
      bundles = new ArrayList<Bundle>();
      // Get bundles
      TableRowIterator tri = DatabaseManager.queryTable(ourContext, "bundle",
              "SELECT bundle.* FROM bundle, item2bundle WHERE "
              + "item2bundle.bundle_id=bundle.bundle_id AND "
              + "item2bundle.item_id= ? ",
              itemRow.getIntColumn("item_id"));

      try {
        while (tri.hasNext()) {
          TableRow r = tri.next();

          // First check the cache
          Bundle fromCache = (Bundle) ourContext.fromCache(Bundle.class,
                  r.getIntColumn("bundle_id"));

          if (fromCache != null) {
            bundles.add(fromCache);
          } else {
            bundles.add(new Bundle(ourContext, r));
          }
        }
      } finally {
        // close the TableRowIterator to free up resources
        if (tri != null) {
          tri.close();
        }
      }
    }

    Bundle[] bundleArray = new Bundle[bundles.size()];
    bundleArray = (Bundle[]) bundles.toArray(bundleArray);

    return bundleArray;
  }

  /**
   * Get the bundles matching a bundle name (name corresponds roughly to type)
   *
   * @param name name of bundle (ORIGINAL/TEXT/THUMBNAIL)
   *
   * @return the bundles in an unordered array
   */
  public Bundle[] getBundles(String name) throws SQLException {
    List<Bundle> matchingBundles = new ArrayList<Bundle>();

    // now only keep bundles with matching names
    Bundle[] bunds = getBundles();
    for (int i = 0; i < bunds.length; i++) {
      if (name.equals(bunds[i].getName())) {
        matchingBundles.add(bunds[i]);
      }
    }

    Bundle[] bundleArray = new Bundle[matchingBundles.size()];
    bundleArray = (Bundle[]) matchingBundles.toArray(bundleArray);

    return bundleArray;
  }

  /**
   * Create a bundle in this item, with immediate effect
   *
   * @param name bundle name (ORIGINAL/TEXT/THUMBNAIL)
   * @return the newly created bundle
   * @throws SQLException
   * @throws AuthorizeException
   */
  public Bundle createBundle(String name) throws SQLException,
          AuthorizeException {
    if ((name == null) || "".equals(name)) {
      throw new SQLException("Bundle must be created with non-null name");
    }

    // Check authorisation
    AuthorizeManager.authorizeAction(ourContext, this, Constants.ADD);

    Bundle b = Bundle.create(ourContext);
    b.setName(name);
    b.update();

    addBundle(b);

    return b;
  }

  /**
   * Add an existing bundle to this item. This has immediate effect.
   *
   * @param b the bundle to add
   * @throws SQLException
   * @throws AuthorizeException
   */
  public void addBundle(Bundle b) throws SQLException, AuthorizeException {
    // Check authorisation
    AuthorizeManager.authorizeAction(ourContext, this, Constants.ADD);

    log.info(LogManager.getHeader(ourContext, "add_bundle", "item_id="
            + getID() + ",bundle_id=" + b.getID()));

    // Check it's not already there
    Bundle[] bunds = getBundles();
    for (int i = 0; i < bunds.length; i++) {
      if (b.getID() == bunds[i].getID()) {
        // Bundle is already there; no change
        return;
      }
    }

    // now add authorization policies from owning item
    // hmm, not very "multiple-inclusion" friendly
    AuthorizeManager.inheritPolicies(ourContext, this, b);

    // Add the bundle to in-memory list
    bundles.add(b);

    // Insert the mapping
    TableRow mappingRow = DatabaseManager.row("item2bundle");
    mappingRow.setColumn("item_id", getID());
    mappingRow.setColumn("bundle_id", b.getID());
    DatabaseManager.insert(ourContext, mappingRow);

    ourContext.addEvent(new Event(Event.ADD, Constants.ITEM, getID(), Constants.BUNDLE, b.getID(), b.getName()));
  }

  /**
   * Remove a bundle. This may result in the bundle being deleted, if the bundle
   * is orphaned.
   *
   * @param b the bundle to remove
   * @throws SQLException
   * @throws AuthorizeException
   * @throws IOException
   */
  public void removeBundle(Bundle b) throws SQLException, AuthorizeException,
          IOException {
    // Check authorisation
    AuthorizeManager.authorizeAction(ourContext, this, Constants.REMOVE);

    log.info(LogManager.getHeader(ourContext, "remove_bundle", "item_id="
            + getID() + ",bundle_id=" + b.getID()));

    // Remove from internal list of bundles
    Bundle[] bunds = getBundles();

    for (int i = 0; i < bunds.length; i++) {
      if (b.getID() == bunds[i].getID()) {
        // We've found the bundle to remove
        bundles.remove(bunds[i]);
        break;
      }
    }

    // Remove mapping from DB
    DatabaseManager.updateQuery(ourContext,
            "DELETE FROM item2bundle WHERE item_id= ? "
            + "AND bundle_id= ? ",
            getID(), b.getID());

    ourContext.addEvent(new Event(Event.REMOVE, Constants.ITEM, getID(), Constants.BUNDLE, b.getID(), b.getName()));

    // If the bundle is orphaned, it's removed
    TableRowIterator tri = DatabaseManager.query(ourContext,
            "SELECT * FROM item2bundle WHERE bundle_id= ? ",
            b.getID());

    try {
      if (!tri.hasNext()) {
        //make the right to remove the bundle explicit because the implicit
        // relation
        //has been removed. This only has to concern the currentUser
        // because
        //he started the removal process and he will end it too.
        //also add right to remove from the bundle to remove it's
        // bitstreams.
        AuthorizeManager.addPolicy(ourContext, b, Constants.DELETE,
                ourContext.getCurrentUser());
        AuthorizeManager.addPolicy(ourContext, b, Constants.REMOVE,
                ourContext.getCurrentUser());

        // The bundle is an orphan, delete it
        b.delete();
      }
    } finally {
      // close the TableRowIterator to free up resources
      if (tri != null) {
        tri.close();
      }
    }
  }

  /**
   * Create a single bitstream in a new bundle. Provided as a convenience method
   * for the most common use.
   *
   * @param is the stream to create the new bitstream from
   * @param name is the name of the bundle (ORIGINAL, TEXT, THUMBNAIL)
   * @return Bitstream that is created
   * @throws AuthorizeException
   * @throws IOException
   * @throws SQLException
   */
  public Bitstream createSingleBitstream(InputStream is, String name)
          throws AuthorizeException, IOException, SQLException {
    // Authorisation is checked by methods below
    // Create a bundle
    Bundle bnd = createBundle(name);
    Bitstream bitstream = bnd.createBitstream(is);
    addBundle(bnd);

    // FIXME: Create permissions for new bundle + bitstream
    return bitstream;
  }

  /**
   * Convenience method, calls createSingleBitstream() with name "ORIGINAL"
   *
   * @param is InputStream
   * @return created bitstream
   * @throws AuthorizeException
   * @throws IOException
   * @throws SQLException
   */
  public Bitstream createSingleBitstream(InputStream is)
          throws AuthorizeException, IOException, SQLException {
    return createSingleBitstream(is, "ORIGINAL");
  }

  /**
   * Get all non-internal bitstreams in the item. This is mainly used for
   * auditing for provenance messages and adding format.* DC values. The order
   * is indeterminate.
   *
   * @return non-internal bitstreams.
   */
  public Bitstream[] getNonInternalBitstreams() throws SQLException {
    List<Bitstream> bitstreamList = new ArrayList<Bitstream>();

    // Go through the bundles and bitstreams picking out ones which aren't
    // of internal formats
    Bundle[] bunds = getBundles();

    for (int i = 0; i < bunds.length; i++) {
      Bitstream[] bitstreams = bunds[i].getBitstreams();

      for (int j = 0; j < bitstreams.length; j++) {
        if (!bitstreams[j].getFormat().isInternal()) {
          // Bitstream is not of an internal format
          bitstreamList.add(bitstreams[j]);
        }
      }
    }

    return bitstreamList.toArray(new Bitstream[bitstreamList.size()]);
  }

  /**
   * Remove just the DSpace license from an item This is useful to update the
   * current DSpace license, in case the user must accept the DSpace license
   * again (either the item was rejected, or resumed after saving) <p> This
   * method is used by the org.dspace.submit.step.LicenseStep class
   *
   * @throws SQLException
   * @throws AuthorizeException
   * @throws IOException
   */
  public void removeDSpaceLicense() throws SQLException, AuthorizeException,
          IOException {
    // get all bundles with name "LICENSE" (these are the DSpace license
    // bundles)
    Bundle[] bunds = getBundles("LICENSE");

    for (int i = 0; i < bunds.length; i++) {
      // FIXME: probably serious troubles with Authorizations
      // fix by telling system not to check authorization?
      removeBundle(bunds[i]);
    }
  }

  /**
   * Remove all licenses from an item - it was rejected
   *
   * @throws SQLException
   * @throws AuthorizeException
   * @throws IOException
   */
  public void removeLicenses() throws SQLException, AuthorizeException,
          IOException {
    // Find the License format
    BitstreamFormat bf = BitstreamFormat.findByShortDescription(ourContext,
            "License");
    int licensetype = bf.getID();

    // search through bundles, looking for bitstream type license
    Bundle[] bunds = getBundles();

    for (int i = 0; i < bunds.length; i++) {
      boolean removethisbundle = false;

      Bitstream[] bits = bunds[i].getBitstreams();

      for (int j = 0; j < bits.length; j++) {
        BitstreamFormat bft = bits[j].getFormat();

        if (bft.getID() == licensetype) {
          removethisbundle = true;
        }
      }

      // probably serious troubles with Authorizations
      // fix by telling system not to check authorization?
      if (removethisbundle) {
        removeBundle(bunds[i]);
      }
    }
  }

  /**
   * Update the item "in archive" flag and Dublin Core metadata in the database
   *
   * @throws SQLException
   * @throws AuthorizeException
   */
  public void update() throws SQLException, AuthorizeException {
    // Check authorisation
    // only do write authorization if user is not an editor
    if (!canEdit()) {
      AuthorizeManager.authorizeAction(ourContext, this, Constants.WRITE);
    }

    log.info(LogManager.getHeader(ourContext, "update_item", "item_id="
            + getID()));

    // Set sequence IDs for bitstreams in item
    int sequence = 0;
    Bundle[] bunds = getBundles();

    // find the highest current sequence number
    for (int i = 0; i < bunds.length; i++) {
      Bitstream[] streams = bunds[i].getBitstreams();

      for (int k = 0; k < streams.length; k++) {
        if (streams[k].getSequenceID() > sequence) {
          sequence = streams[k].getSequenceID();
        }
      }
    }

    // start sequencing bitstreams without sequence IDs
    sequence++;

    for (int i = 0; i < bunds.length; i++) {
      Bitstream[] streams = bunds[i].getBitstreams();

      for (int k = 0; k < streams.length; k++) {
        if (streams[k].getSequenceID() < 0) {
          streams[k].setSequenceID(sequence);
          sequence++;
          streams[k].update();
          modified = true;
        }
      }
    }

    // Map counting number of values for each element/qualifier.
    // Keys are Strings: "element" or "element.qualifier"
    // Values are Integers indicating number of values written for a
    // element/qualifier
    Map<String, Integer> elementCount = new HashMap<String, Integer>();

    // Redo Dublin Core if it's changed
    if (dublinCoreChanged) {
      dublinCoreChanged = false;

      // Arrays to store the working information required
      int[] placeNum = new int[getMetadata().size()];
      boolean[] storedDC = new boolean[getMetadata().size()];
      MetadataField[] dcFields = new MetadataField[getMetadata().size()];

      // Work out the place numbers for the in memory DC
      for (int dcIdx = 0; dcIdx < getMetadata().size(); dcIdx++) {
        DCValue dcv = getMetadata().get(dcIdx);

        // Work out the place number for ordering
        int current = 0;

        // Key into map is "element" or "element.qualifier"
        String key = dcv.element + ((dcv.qualifier == null) ? "" : ("." + dcv.qualifier));

        Integer currentInteger = elementCount.get(key);
        if (currentInteger != null) {
          current = currentInteger.intValue();
        }

        current++;
        elementCount.put(key, Integer.valueOf(current));

        // Store the calculated place number, reset the stored flag, and cache the metadatafield
        placeNum[dcIdx] = current;
        storedDC[dcIdx] = false;
        dcFields[dcIdx] = getMetadataField(dcv);
        if (dcFields[dcIdx] == null) {
          // Bad DC field, log and throw exception
          log.warn(LogManager.getHeader(ourContext, "bad_dc",
                  "Bad DC field. schema=" + dcv.schema
                  + ", element: \""
                  + ((dcv.element == null) ? "null"
                  : dcv.element)
                  + "\" qualifier: \""
                  + ((dcv.qualifier == null) ? "null"
                  : dcv.qualifier)
                  + "\" value: \""
                  + ((dcv.value == null) ? "null"
                  : dcv.value) + "\""));

          throw new SQLException("bad_dublin_core "
                  + "schema=" + dcv.schema + ", "
                  + dcv.element
                  + " " + dcv.qualifier);
        }
      }

      // Now the precalculations are done, iterate through the existing metadata
      // looking for matches
      TableRowIterator tri = retrieveMetadata();
      if (tri != null) {
        try {
          while (tri.hasNext()) {
            TableRow tr = tri.next();
            // Assume that we will remove this row, unless we get a match
            boolean removeRow = true;

            // Go through the in-memory metadata, unless we've already decided to keep this row
            for (int dcIdx = 0; dcIdx < getMetadata().size() && removeRow; dcIdx++) {
              // Only process if this metadata has not already been matched to something in the DB
              if (!storedDC[dcIdx]) {
                boolean matched = true;
                DCValue dcv = getMetadata().get(dcIdx);

                // Check the metadata field is the same
                if (matched && dcFields[dcIdx].getFieldID() != tr.getIntColumn("metadata_field_id")) {
                  matched = false;
                }

                // Check the place is the same
                if (matched && placeNum[dcIdx] != tr.getIntColumn("place")) {
                  matched = false;
                }

                // Check the text is the same
                if (matched) {
                  String text = tr.getStringColumn("text_value");
                  if (dcv.value == null && text == null) {
                    matched = true;
                  } else if (dcv.value != null && dcv.value.equals(text)) {
                    matched = true;
                  } else {
                    matched = false;
                  }
                }

                // Check the language is the same
                if (matched) {
                  String lang = tr.getStringColumn("text_lang");
                  if (dcv.language == null && lang == null) {
                    matched = true;
                  } else if (dcv.language != null && dcv.language.equals(lang)) {
                    matched = true;
                  } else {
                    matched = false;
                  }
                }

                // check that authority and confidence match
                if (matched) {
                  String auth = tr.getStringColumn("authority");
                  int conf = tr.getIntColumn("confidence");
                  if (!((dcv.authority == null && auth == null)
                          || (dcv.authority != null && auth != null && dcv.authority.equals(auth))
                          && dcv.confidence == conf)) {
                    matched = false;
                  }
                }

                // If the db record is identical to the in memory values
                if (matched) {
                  // Flag that the metadata is already in the DB
                  storedDC[dcIdx] = true;

                  // Flag that we are not going to remove the row
                  removeRow = false;
                }
              }
            }

            // If after processing all the metadata values, we didn't find a match
            // delete this row from the DB
            if (removeRow) {
              DatabaseManager.delete(ourContext, tr);
              dublinCoreChanged = true;
              modified = true;
            }
          }
        } finally {
          tri.close();
        }
      }

      // Add missing in-memory DC
      for (int dcIdx = 0; dcIdx < getMetadata().size(); dcIdx++) {
        // Only write values that are not already in the db
        if (!storedDC[dcIdx]) {
          DCValue dcv = getMetadata().get(dcIdx);

          // Write DCValue
          MetadataValue metadata = new MetadataValue();
          metadata.setItemId(getID());
          metadata.setFieldId(dcFields[dcIdx].getFieldID());
          metadata.setValue(dcv.value);
          metadata.setLanguage(dcv.language);
          metadata.setPlace(placeNum[dcIdx]);
          metadata.setAuthority(dcv.authority);
          metadata.setConfidence(dcv.confidence);
          metadata.create(ourContext);
          dublinCoreChanged = true;
          modified = true;
        }
      }
    }

    if (dublinCoreChanged || modified) {
      // Set the last modified date
      itemRow.setColumn("last_modified", new Date());

      // Make sure that withdrawn and in_archive are non-null
      if (itemRow.isColumnNull("in_archive")) {
        itemRow.setColumn("in_archive", false);
      }

      if (itemRow.isColumnNull("withdrawn")) {
        itemRow.setColumn("withdrawn", false);
      }

      DatabaseManager.update(ourContext, itemRow);

      if (dublinCoreChanged) {
        ourContext.addEvent(new Event(Event.MODIFY_METADATA, Constants.ITEM, getID(), getDetails()));
        clearDetails();
        dublinCoreChanged = false;
      }

      ourContext.addEvent(new Event(Event.MODIFY, Constants.ITEM, getID(), null));
      modified = false;
    }
//    this.updateLicense();
//    this.updateCitationString();
//    this.updateISSN();
//    this.updateSubjectFields();
  }
  private transient MetadataField[] allMetadataFields = null;

  private MetadataField getMetadataField(DCValue dcv) throws SQLException, AuthorizeException {
    if (allMetadataFields == null) {
      allMetadataFields = MetadataField.findAll(ourContext);
    }

    if (allMetadataFields != null) {
      int schemaID = getMetadataSchemaID(dcv);
      for (MetadataField field : allMetadataFields) {
        if (field.getSchemaID() == schemaID
                && StringUtils.equals(field.getElement(), dcv.element)
                && StringUtils.equals(field.getQualifier(), dcv.qualifier)) {
          return field;
        }
      }
    }

    return null;
  }

  private int getMetadataSchemaID(DCValue dcv) throws SQLException {
    int schemaID;
    MetadataSchema schema = MetadataSchema.find(ourContext, dcv.schema);
    if (schema == null) {
      schemaID = MetadataSchema.DC_SCHEMA_ID;
    } else {
      schemaID = schema.getSchemaID();
    }
    return schemaID;
  }

  /**
   * Withdraw the item from the archive. It is kept in place, and the content
   * and metadata are not deleted, but it is not publicly accessible.
   *
   * @throws SQLException
   * @throws AuthorizeException
   * @throws IOException
   */
  public void withdraw() throws SQLException, AuthorizeException, IOException {
    // Check permission. User either has to have REMOVE on owning collection
    // or be COLLECTION_EDITOR of owning collection
    AuthorizeUtil.authorizeWithdrawItem(ourContext, this);

    String timestamp = DCDate.getCurrent().toString();

    // Add suitable provenance - includes user, date, collections +
    // bitstream checksums
    EPerson e = ourContext.getCurrentUser();

    // Build some provenance data while we're at it.
    StringBuilder prov = new StringBuilder();

    prov.append("Item withdrawn by ").append(e.getFullName()).append(" (").append(e.getEmail()).append(") on ").append(timestamp).append("\n").append("Item was in collections:\n");

    Collection[] colls = getCollections();

    for (int i = 0; i < colls.length; i++) {
      prov.append(colls[i].getMetadata("name")).append(" (ID: ").append(colls[i].getID()).append(")\n");
    }

    // Set withdrawn flag. timestamp will be set; last_modified in update()
    itemRow.setColumn("withdrawn", true);

    // in_archive flag is now false
    itemRow.setColumn("in_archive", false);

    prov.append(InstallItem.getBitstreamProvenanceMessage(this));

    addDC("description", "provenance", "en", prov.toString());

    // Update item in DB
    update();

    ourContext.addEvent(new Event(Event.MODIFY, Constants.ITEM, getID(), "WITHDRAW"));

    // and all of our authorization policies
    // FIXME: not very "multiple-inclusion" friendly
    AuthorizeManager.removeAllPolicies(ourContext, this);

    // Write log
    log.info(LogManager.getHeader(ourContext, "withdraw_item", "user="
            + e.getEmail() + ",item_id=" + getID()));
  }

  /**
   * Reinstate a withdrawn item
   *
   * @throws SQLException
   * @throws AuthorizeException
   * @throws IOException
   */
  public void reinstate() throws SQLException, AuthorizeException,
          IOException {
    // check authorization
    AuthorizeUtil.authorizeReinstateItem(ourContext, this);

    String timestamp = DCDate.getCurrent().toString();

    // Check permission. User must have ADD on all collections.
    // Build some provenance data while we're at it.
    Collection[] colls = getCollections();

    // Add suitable provenance - includes user, date, collections +
    // bitstream checksums
    EPerson e = ourContext.getCurrentUser();
    StringBuilder prov = new StringBuilder();
    prov.append("Item reinstated by ").append(e.getFullName()).append(" (").append(e.getEmail()).append(") on ").append(timestamp).append("\n").append("Item was in collections:\n");

    for (int i = 0; i < colls.length; i++) {
      prov.append(colls[i].getMetadata("name")).append(" (ID: ").append(colls[i].getID()).append(")\n");
    }

    // Clear withdrawn flag
    itemRow.setColumn("withdrawn", false);

    // in_archive flag is now true
    itemRow.setColumn("in_archive", true);

    // Add suitable provenance - includes user, date, collections +
    // bitstream checksums
    prov.append(InstallItem.getBitstreamProvenanceMessage(this));

    addDC("description", "provenance", "en", prov.toString());

    // Update item in DB
    update();

    ourContext.addEvent(new Event(Event.MODIFY, Constants.ITEM, getID(), "REINSTATE"));

    // authorization policies
    if (colls.length > 0) {
      // FIXME: not multiple inclusion friendly - just apply access
      // policies from first collection
      // remove the item's policies and replace them with
      // the defaults from the collection
      inheritCollectionDefaultPolicies(colls[0]);
    }

    // Write log
    log.info(LogManager.getHeader(ourContext, "reinstate_item", "user="
            + e.getEmail() + ",item_id=" + getID()));
  }

  /**
   * Delete (expunge) the item. Bundles and bitstreams are also deleted if they
   * are not also included in another item. The Dublin Core metadata is deleted.
   *
   * @throws SQLException
   * @throws AuthorizeException
   * @throws IOException
   */
  void delete() throws SQLException, AuthorizeException, IOException {
    // Check authorisation here. If we don't, it may happen that we remove the
    // metadata but when getting to the point of removing the bundles we get an exception
    // leaving the database in an inconsistent state
    AuthorizeManager.authorizeAction(ourContext, this, Constants.REMOVE);

    ourContext.addEvent(new Event(Event.DELETE, Constants.ITEM, getID(), getHandle()));

    log.info(LogManager.getHeader(ourContext, "delete_item", "item_id="
            + getID()));

    // Remove from cache
    ourContext.removeCached(this, getID());

    // Remove from browse indices, if appropriate
    /**
     * XXX FIXME * Although all other Browse index updates are managed through *
     * Event consumers, removing an Item *must* be done *here* (inline) *
     * because otherwise, tables are left in an inconsistent state * and the DB
     * transaction will fail. * Any fix would involve too much work on Browse
     * code that * is likely to be replaced soon anyway. --lcs, Aug 2006 * * NB
     * Do not check to see if the item is archived - withdrawn / * non-archived
     * items may still be tracked in some browse tables * for administrative
     * purposes, and these need to be removed.
     *
     */
//               FIXME: there is an exception handling problem here
    try {
//               Remove from indices
      IndexBrowse ib = new IndexBrowse(ourContext);
      ib.itemRemoved(this);
    } catch (BrowseException e) {
      log.error("caught exception: ", e);
      throw new SQLException(e.getMessage(), e);
    }

    // Delete the Dublin Core
    removeMetadataFromDatabase();

    // Remove bundles
    Bundle[] bunds = getBundles();

    for (int i = 0; i < bunds.length; i++) {
      removeBundle(bunds[i]);
    }

    // remove all of our authorization policies
    AuthorizeManager.removeAllPolicies(ourContext, this);

    // Remove any Handle
    HandleManager.unbindHandle(ourContext, this);

    // Finally remove item row
    DatabaseManager.delete(ourContext, itemRow);
  }

  /**
   * Remove item and all its sub-structure from the context cache. Useful in
   * batch processes where a single context has a long, multi-item lifespan
   */
  public void decache() throws SQLException {
    // Remove item and it's submitter from cache
    ourContext.removeCached(this, getID());
    if (submitter != null) {
      ourContext.removeCached(submitter, submitter.getID());
    }
    // Remove bundles & bitstreams from cache if they have been loaded
    if (bundles != null) {
      Bundle[] bunds = getBundles();
      for (int i = 0; i < bunds.length; i++) {
        ourContext.removeCached(bunds[i], bunds[i].getID());
        Bitstream[] bitstreams = bunds[i].getBitstreams();
        for (int j = 0; j < bitstreams.length; j++) {
          ourContext.removeCached(bitstreams[j], bitstreams[j].getID());
        }
      }
    }
  }

  /**
   * Return
   * <code>true</code> if
   * <code>other</code> is the same Item as this object,
   * <code>false</code> otherwise
   *
   * @param obj object to compare to
   * @return <code>true</code> if object passed in represents the same item as
   * this object
   */
  @Override
  public boolean equals(Object obj) {
    if (obj == null) {
      return false;
    }
    if (getClass() != obj.getClass()) {
      return false;
    }
    final Item other = (Item) obj;
    if (this.getType() != other.getType()) {
      return false;
    }
    if (this.getID() != other.getID()) {
      return false;
    }

    return true;
  }

  @Override
  public int hashCode() {
    int hash = 5;
    hash = 71 * hash + (this.itemRow != null ? this.itemRow.hashCode() : 0);
    return hash;
  }

  /**
   * Return true if this Collection 'owns' this item
   *
   * @param c Collection
   * @return true if this Collection owns this item
   */
  public boolean isOwningCollection(Collection c) {
    int owner_id = itemRow.getIntColumn("owning_collection");

    if (c.getID() == owner_id) {
      return true;
    }

    // not the owner
    return false;
  }

  /**
   * Utility method to remove all descriptive metadata associated with the item
   * from the database (regardless of in-memory version)
   *
   * @throws SQLException
   */
  private void removeMetadataFromDatabase() throws SQLException {
    DatabaseManager.updateQuery(ourContext,
            "DELETE FROM MetadataValue WHERE item_id= ? ",
            getID());
  }

  /**
   * return type found in Constants
   *
   * @return int Constants.ITEM
   */
  public int getType() {
    return Constants.ITEM;
  }

  /**
   * remove all of the policies for item and replace them with a new list of
   * policies
   *
   * @param newpolicies - this will be all of the new policies for the item and
   * its contents
   * @throws SQLException
   * @throws AuthorizeException
   */
  public void replaceAllItemPolicies(List<ResourcePolicy> newpolicies) throws SQLException,
          AuthorizeException {
    // remove all our policies, add new ones
    AuthorizeManager.removeAllPolicies(ourContext, this);
    AuthorizeManager.addPolicies(ourContext, newpolicies, this);
  }

  /**
   * remove all of the policies for item's bitstreams and bundles and replace
   * them with a new list of policies
   *
   * @param newpolicies - this will be all of the new policies for the bundle
   * and bitstream contents
   * @throws SQLException
   * @throws AuthorizeException
   */
  public void replaceAllBitstreamPolicies(List<ResourcePolicy> newpolicies)
          throws SQLException, AuthorizeException {
    // remove all policies from bundles, add new ones
    // Remove bundles
    Bundle[] bunds = getBundles();

    for (int i = 0; i < bunds.length; i++) {
      Bundle mybundle = bunds[i];
      mybundle.replaceAllBitstreamPolicies(newpolicies);
    }
  }

  /**
   * remove all of the policies for item's bitstreams and bundles that belong to
   * a given Group
   *
   * @param g Group referenced by policies that needs to be removed
   * @throws SQLException
   */
  public void removeGroupPolicies(Group g) throws SQLException {
    // remove Group's policies from Item
    AuthorizeManager.removeGroupPolicies(ourContext, this, g);

    // remove all policies from bundles
    Bundle[] bunds = getBundles();

    for (int i = 0; i < bunds.length; i++) {
      Bundle mybundle = bunds[i];

      Bitstream[] bs = mybundle.getBitstreams();

      for (int j = 0; j < bs.length; j++) {
        // remove bitstream policies
        AuthorizeManager.removeGroupPolicies(ourContext, bs[j], g);
      }

      // change bundle policies
      AuthorizeManager.removeGroupPolicies(ourContext, mybundle, g);
    }
  }

  /**
   * remove all policies on an item and its contents, and replace them with the
   * DEFAULT_ITEM_READ and DEFAULT_BITSTREAM_READ policies belonging to the
   * collection.
   *
   * @param c Collection
   * @throws java.sql.SQLException if an SQL error or if no default policies
   * found. It's a bit draconian, but default policies must be enforced.
   * @throws AuthorizeException
   */
  public void inheritCollectionDefaultPolicies(Collection c)
          throws java.sql.SQLException, AuthorizeException {
    // remove the submit authorization policies
    // and replace them with the collection's default READ policies
    List<ResourcePolicy> policies = AuthorizeManager.getPoliciesActionFilter(ourContext, c, Constants.DEFAULT_ITEM_READ);

    // MUST have default policies
    if (policies.size() < 1) {
      throw new java.sql.SQLException("Collection " + c.getID()
              + " has no default item READ policies");
    }

    // change the action to just READ
    // just don't call update on the resourcepolicies!!!
    for (ResourcePolicy rp : policies) {
      rp.setAction(Constants.READ);
    }

    replaceAllItemPolicies(policies);

    policies = AuthorizeManager.getPoliciesActionFilter(ourContext, c, Constants.DEFAULT_BITSTREAM_READ);

    if (policies.size() < 1) {
      throw new java.sql.SQLException("Collection " + c.getID()
              + " has no default bitstream READ policies");
    }

    // change the action to just READ
    // just don't call update on the resourcepolicies!!!
    for (ResourcePolicy rp : policies) {
      rp.setAction(Constants.READ);
    }

    replaceAllBitstreamPolicies(policies);

    log.debug(LogManager.getHeader(ourContext, "item_inheritCollectionDefaultPolicies",
            "item_id=" + getID()));
  }

  /**
   * Moves the item from one collection to another one
   *
   * @throws SQLException
   * @throws AuthorizeException
   * @throws IOException
   */
  public void move(Collection from, Collection to) throws SQLException, AuthorizeException, IOException {
    // Use the normal move method, and default to not inherit permissions
    this.move(from, to, false);
  }

  /**
   * Moves the item from one collection to another one
   *
   * @throws SQLException
   * @throws AuthorizeException
   * @throws IOException
   */
  public void move(Collection from, Collection to, boolean inheritDefaultPolicies) throws SQLException, AuthorizeException, IOException {
    // Check authorisation on the item before that the move occur
    // otherwise we will need edit permission on the "target collection" to archive our goal
    // only do write authorization if user is not an editor
    if (!canEdit()) {
      AuthorizeManager.authorizeAction(ourContext, this, Constants.WRITE);
    }

    // Move the Item from one Collection to the other
    to.addItem(this);
    from.removeItem(this);

    // If we are moving from the owning collection, update that too
    if (isOwningCollection(from)) {
      // Update the owning collection
      log.info(LogManager.getHeader(ourContext, "move_item",
              "item_id=" + getID() + ", from "
              + "collection_id=" + from.getID() + " to "
              + "collection_id=" + to.getID()));
      setOwningCollection(to);

      // If applicable, update the item policies
      if (inheritDefaultPolicies) {
        log.info(LogManager.getHeader(ourContext, "move_item",
                "Updating item with inherited policies"));
        inheritCollectionDefaultPolicies(to);
      }

      // Update the item
      ourContext.turnOffAuthorisationSystem();
      update();
      ourContext.restoreAuthSystemState();
    } else {
      // Although we haven't actually updated anything within the item
      // we'll tell the event system that it has, so that any consumers that
      // care about the structure of the repository can take account of the move

      // Note that updating the owning collection above will have the same effect,
      // so we only do this here if the owning collection hasn't changed.

      ourContext.addEvent(new Event(Event.MODIFY, Constants.ITEM, getID(), null));
    }
  }

  /**
   * Check the bundle ORIGINAL to see if there are any uploaded files
   *
   * @return true if there is a bundle named ORIGINAL with one or more
   * bitstreams inside
   * @throws SQLException
   */
  public boolean hasUploadedFiles() throws SQLException {
    Bundle[] bundles = getBundles("ORIGINAL");
    if (bundles.length == 0) {
      // if no ORIGINAL bundle,
      // return false that there is no file!
      return false;
    } else {
      Bitstream[] bitstreams = bundles[0].getBitstreams();
      if (bitstreams.length == 0) {
        // no files in ORIGINAL bundle!
        return false;
      }
    }
    return true;
  }

  /**
   * Get the collections this item is not in.
   *
   * @return the collections this item is not in, if any.
   * @throws SQLException
   */
  public Collection[] getCollectionsNotLinked() throws SQLException {
    Collection[] allCollections = Collection.findAll(ourContext);
    Collection[] linkedCollections = getCollections();
    Collection[] notLinkedCollections = new Collection[allCollections.length - linkedCollections.length];

    if ((allCollections.length - linkedCollections.length) == 0) {
      return notLinkedCollections;
    }

    int i = 0;

    for (Collection collection : allCollections) {
      boolean alreadyLinked = false;

      for (Collection linkedCommunity : linkedCollections) {
        if (collection.getID() == linkedCommunity.getID()) {
          alreadyLinked = true;
          break;
        }
      }

      if (!alreadyLinked) {
        notLinkedCollections[i++] = collection;
      }
    }

    return notLinkedCollections;
  }

  /**
   * return TRUE if context's user can edit item, false otherwise
   *
   * @return boolean true = current user can edit item
   * @throws SQLException
   */
  public boolean canEdit() throws java.sql.SQLException {
    // can this person write to the item?
    if (AuthorizeManager.authorizeActionBoolean(ourContext, this,
            Constants.WRITE)) {
      return true;
    }

    // is this collection not yet created, and an item template is created
    if (getOwningCollection() == null) {
      return true;
    }

    // is this person an COLLECTION_EDITOR for the owning collection?
    if (getOwningCollection().canEditBoolean(false)) {
      return true;
    }

    return false;
  }

  public String getName() {
    DCValue t[] = getMetadata("dc", "title", null, Item.ANY);
    return (t.length >= 1) ? t[0].value : null;
  }

  /**
   * Returns an iterator of Items possessing the passed metadata field, or only
   * those matching the passed value, if value is not Item.ANY
   *
   * @param context DSpace context object
   * @param schema metadata field schema
   * @param element metadata field element
   * @param qualifier metadata field qualifier
   * @param value field value or Item.ANY to match any value
   * @return an iterator over the items matching that authority value
   * @throws SQLException, AuthorizeException, IOException
   *
   */
  public static ItemIterator findByMetadataField(Context context,
          String schema, String element, String qualifier, String value)
          throws SQLException, AuthorizeException, IOException {
    MetadataSchema mds = MetadataSchema.find(context, schema);
    if (mds == null) {
      throw new IllegalArgumentException("No such metadata schema: " + schema);
    }
    MetadataField mdf = MetadataField.findByElement(context, mds.getSchemaID(), element, qualifier);
    if (mdf == null) {
      throw new IllegalArgumentException(
              "No such metadata field: schema=" + schema + ", element=" + element + ", qualifier=" + qualifier);
    }

    String query = "SELECT item.* FROM metadatavalue,item WHERE item.in_archive='1' "
            + "AND item.item_id = metadatavalue.item_id AND metadata_field_id = ?";
    TableRowIterator rows = null;
    if (Item.ANY.equals(value)) {
      rows = DatabaseManager.queryTable(context, "item", query, mdf.getFieldID());
    } else {
      query += " AND metadatavalue.text_value = ?";
      rows = DatabaseManager.queryTable(context, "item", query, mdf.getFieldID(), value);
    }
    return new ItemIterator(context, rows);
  }

  public DSpaceObject getAdminObject(int action) throws SQLException {
    DSpaceObject adminObject = null;
    Collection collection = getOwningCollection();
    Community community = null;
    if (collection != null) {
      Community[] communities = collection.getCommunities();
      if (communities != null && communities.length > 0) {
        community = communities[0];
      }
    } else {
      // is a template item?
      TableRow qResult = DatabaseManager.querySingle(ourContext,
              "SELECT collection_id FROM collection "
              + "WHERE template_item_id = ?", getID());
      if (qResult != null) {
        collection = Collection.find(ourContext, qResult.getIntColumn("collection_id"));
        Community[] communities = collection.getCommunities();
        if (communities != null && communities.length > 0) {
          community = communities[0];
        }
      }
    }

    switch (action) {
      case Constants.ADD:
        // ADD a cc license is less general than add a bitstream but we can't/won't
        // add complex logic here to know if the ADD action on the item is required by a cc or
        // a generic bitstream so simply we ignore it.. UI need to enforce the requirements.
        if (AuthorizeConfiguration.canItemAdminPerformBitstreamCreation()) {
          adminObject = this;
        } else if (AuthorizeConfiguration.canCollectionAdminPerformBitstreamCreation()) {
          adminObject = collection;
        } else if (AuthorizeConfiguration.canCommunityAdminPerformBitstreamCreation()) {
          adminObject = community;
        }
        break;
      case Constants.REMOVE:
        // see comments on ADD action, same things...
        if (AuthorizeConfiguration.canItemAdminPerformBitstreamDeletion()) {
          adminObject = this;
        } else if (AuthorizeConfiguration.canCollectionAdminPerformBitstreamDeletion()) {
          adminObject = collection;
        } else if (AuthorizeConfiguration.canCommunityAdminPerformBitstreamDeletion()) {
          adminObject = community;
        }
        break;
      case Constants.DELETE:
        if (getOwningCollection() != null) {
          if (AuthorizeConfiguration.canCollectionAdminPerformItemDeletion()) {
            adminObject = collection;
          } else if (AuthorizeConfiguration.canCommunityAdminPerformItemDeletion()) {
            adminObject = community;
          }
        } else {
          if (AuthorizeConfiguration.canCollectionAdminManageTemplateItem()) {
            adminObject = collection;
          } else if (AuthorizeConfiguration.canCommunityAdminManageCollectionTemplateItem()) {
            adminObject = community;
          }
        }
        break;
      case Constants.WRITE:
        // if it is a template item we need to check the
        // collection/community admin configuration
        if (getOwningCollection() == null) {
          if (AuthorizeConfiguration.canCollectionAdminManageTemplateItem()) {
            adminObject = collection;
          } else if (AuthorizeConfiguration.canCommunityAdminManageCollectionTemplateItem()) {
            adminObject = community;
          }
        } else {
          adminObject = this;
        }
        break;
      default:
        adminObject = this;
        break;
    }
    return adminObject;
  }

  public DSpaceObject getParentObject() throws SQLException {
    Collection ownCollection = getOwningCollection();
    if (ownCollection != null) {
      return ownCollection;
    } else {
      // is a template item?
      TableRow qResult = DatabaseManager.querySingle(ourContext,
              "SELECT collection_id FROM collection "
              + "WHERE template_item_id = ?", getID());
      if (qResult != null) {
        return Collection.find(ourContext, qResult.getIntColumn("collection_id"));
      }
      return null;
    }
  }

  /**
   * Find all the items in the archive with a given authority key value in the
   * indicated metadata field.
   *
   * @param context DSpace context object
   * @param schema metadata field schema
   * @param element metadata field element
   * @param qualifier metadata field qualifier
   * @param value the value of authority key to look for
   * @return an iterator over the items matching that authority value
   * @throws SQLException, AuthorizeException, IOException
   */
  public static ItemIterator findByAuthorityValue(Context context,
          String schema, String element, String qualifier, String value)
          throws SQLException, AuthorizeException, IOException {
    MetadataSchema mds = MetadataSchema.find(context, schema);
    if (mds == null) {
      throw new IllegalArgumentException("No such metadata schema: " + schema);
    }
    MetadataField mdf = MetadataField.findByElement(context, mds.getSchemaID(), element, qualifier);
    if (mdf == null) {
      throw new IllegalArgumentException("No such metadata field: schema=" + schema + ", element=" + element + ", qualifier=" + qualifier);
    }

    TableRowIterator rows = DatabaseManager.queryTable(context, "item",
            "SELECT item.* FROM metadatavalue,item WHERE item.in_archive='1' "
            + "AND item.item_id = metadatavalue.item_id AND metadata_field_id = ? AND authority = ?",
            mdf.getFieldID(), value);
    return new ItemIterator(context, rows);
  }

  private List<DCValue> getMetadata() {
    try {
      return dublinCore.get(ourContext, getID(), log);
    } catch (SQLException e) {
      log.error("Loading item - cannot load metadata");
    }

    return new ArrayList<DCValue>();
  }

  private void setMetadata(List<DCValue> metadata) {
    dublinCore.set(metadata);
    dublinCoreChanged = true;
  }

  class MetadataCache {

    List<DCValue> metadata = null;

    List<DCValue> get(Context c, int itemId, Logger log) throws SQLException {
      if (metadata == null) {
        metadata = new ArrayList<DCValue>();

        // Get Dublin Core metadata
        TableRowIterator tri = retrieveMetadata(itemId);

        if (tri != null) {
          try {
            while (tri.hasNext()) {
              TableRow resultRow = tri.next();

              // Get the associated metadata field and schema information
              int fieldID = resultRow.getIntColumn("metadata_field_id");
              MetadataField field = MetadataField.find(c, fieldID);

              if (field == null) {
                log.error("Loading item - cannot find metadata field " + fieldID);
              } else {
                MetadataSchema schema = MetadataSchema.find(c, field.getSchemaID());
                if (schema == null) {
                  log.error("Loading item - cannot find metadata schema " + field.getSchemaID() + ", field " + fieldID);
                } else {
                  // Make a DCValue object
                  DCValue dcv = new DCValue();
                  dcv.element = field.getElement();
                  dcv.qualifier = field.getQualifier();
                  dcv.value = resultRow.getStringColumn("text_value");
                  dcv.language = resultRow.getStringColumn("text_lang");
                  //dcv.namespace = schema.getNamespace();
                  dcv.schema = schema.getName();
                  dcv.authority = resultRow.getStringColumn("authority");
                  dcv.confidence = resultRow.getIntColumn("confidence");

                  // Add it to the list
                  metadata.add(dcv);
                }
              }
            }
          } finally {
            // close the TableRowIterator to free up resources
            if (tri != null) {
              tri.close();
            }
          }
        }
      }

      return metadata;
    }

    void set(List<DCValue> m) {
      metadata = m;
    }

    TableRowIterator retrieveMetadata(int itemId) throws SQLException {
      if (itemId > 0) {
        return DatabaseManager.queryTable(ourContext, "MetadataValue",
                "SELECT * FROM MetadataValue WHERE item_id= ? ORDER BY metadata_field_id, place",
                itemId);
      }

      return null;
    }
  }

  // Edited by Dimitri Surinx START
  public static int[] latestAdditionsId(Context context) throws SQLException {
    String query = "SELECT item.item_id FROM item,handle WHERE item.item_id = handle.resource_id AND handle.resource_type_id = 2 AND in_archive AND NOT withdrawn ORDER BY handle.handle_id DESC ";
    PreparedStatement statement = context.getDBConnection().prepareStatement(query);
    int[] ids = new int[5];

    ResultSet rs = statement.executeQuery();

    for (int i = 0; i < 5 && rs.next(); i++) {

      ids[i] = rs.getInt("item_id");
    }
    return ids;
  }

  public static int returnId(Context context, String element, String qualifier) throws SQLException {
    String query = null;
    PreparedStatement statement = null;
    if (qualifier.equals("")) {
      query = "SELECT metadata_field_id FROM metadatafieldregistry where metadatafieldregistry.element = ?";
      statement = context.getDBConnection().prepareStatement(query);
      statement.setString(1, element);
    } else {
      query = "SELECT metadata_field_id FROM metadatafieldregistry where metadatafieldregistry.element = ? AND metadatafieldregistry.qualifier = ?";
      statement = context.getDBConnection().prepareStatement(query);
      statement.setString(1, element);
      statement.setString(2, qualifier);

    }

    ResultSet rs = statement.executeQuery();
    if (rs.next()) {
      return rs.getInt("metadata_field_id");
    } else {
      return 0;
    }

  }

  public static List<String> latestAdditionsText(Context context, int id, String element, String qualifier) throws SQLException {
    int regId = returnId(context, element, qualifier);
    return latestAdditionsText(context, id, regId);
  }

  public static List<String> latestAdditionsText(Context context, int id, int regId) throws SQLException {
    return latestAdditionsText(context, id, regId, 3);
  }

  public static List<String> latestAdditionsText(Context context, int id, int regId, int amount) throws SQLException {
    String query = "SELECT text_value FROM item item,metadatavalue where item.item_id = metadatavalue.item_id AND item.item_id = ? AND metadata_field_id = ? ORDER BY last_modified DESC";
    PreparedStatement statement = context.getDBConnection().prepareStatement(query);
    List<String> titles = new ArrayList<String>();
    String citation = "";
    statement.setInt(1, id);
    statement.setInt(2, regId);
    int i = 1;

    ResultSet rs = statement.executeQuery();
    for (i = 0; (i < amount || (amount == 0)) && rs.next(); i++) {
      if (i == 0) {
        titles.clear();
      }
      titles.add(rs.getString("text_value"));
    }
    return titles;
  }

  public static String getHandleMod(Context context, int regId) throws SQLException {

    String query = "SELECT handle FROM handle where resource_type_id = 2 AND resource_id = ? ";
    PreparedStatement statement = context.getDBConnection().prepareStatement(query);
    String title = "";
    statement.setInt(1, regId);
    ResultSet rs = statement.executeQuery();

    while (rs.next()) {
      title = rs.getString("handle");
    }
    return title;
  }
  // Edited by Dimitri Surinx STOP

  public boolean updateCitationString() {
    try {
      CitationManager cm = new CitationManager();
      String cit = cm.updateCitationString(this);
      this.update();
      return true;
    } catch (Exception e) {
      log.error("Caught exception in submission step: ", e);
      return false;
    }
  }

  public boolean updateISSN() {
    try {
      DCValue[] titles = getMetadata(MetadataSchema.DC_SCHEMA, "bibliographicCitation", "title", Item.ANY);
      String issn = "";
      if (titles.length > 0) {
        DCValue[] dcvs = getMetadata("dc.identifier.issn");
        if (dcvs.length > 0) {
          if (dcvs[0].value != null) {
            issn = dcvs[0].value;
          }
        }

        if (!"".equals(issn)) {
          for (DCValue jtitle : titles) {
            if (jtitle.value != null) {
              jtitle.authority = issn;
              clearMetadata(MetadataSchema.DC_SCHEMA, "bibliographicCitation", "title", jtitle.language);
              addMetadata(MetadataSchema.DC_SCHEMA, "bibliographicCitation", "title", jtitle.language, jtitle.value, jtitle.authority, Choices.CF_ACCEPTED, true);
            }
          }
        } else {
          issn = titles[0].authority;
          this.clearMetadata(MetadataSchema.DC_SCHEMA, "identifier", "issn", Item.ANY);
          if (issn != null && !"".equals(issn)) {
            this.addMetadata(MetadataSchema.DC_SCHEMA, "identifier", "issn", Item.ANY, issn);
          }
        }
        this.update();
      }
      return true;
    } catch (Exception e) {
      log.error(Item.class.getName() + ": ", e);
      return false;
    }
  }

  public void updateLicense() {
    DCValue[] licenses = this.getMetadata(MetadataSchema.DC_SCHEMA, "rights", "uri", Item.ANY);
    if (licenses.length > 0 && licenses[0].value.equals("custom license")) {
      String localLicenseURL = ConfigurationManager.getProperty("dspace.baseUrl") + "/license";
      //set dc.rights to CC url
      this.clearMetadata(MetadataSchema.DC_SCHEMA, "rights", "uri", Item.ANY);
      this.addMetadata(MetadataSchema.DC_SCHEMA, "rights", "uri", Item.ANY, localLicenseURL);
    }
  }

  private String[] splitFieldName(String fieldname) {
    String[] parts;

    parts = fieldname.split("\\.");

    if (parts.length == 2) {
      String[] result = new String[2];

      result[0] = parts[0];
      result[1] = parts[1];

      return result;
    } else if (parts.length == 3) {
      String[] result = new String[3];

      result[0] = parts[0];
      result[1] = parts[1];
      result[2] = parts[2];

      return result;
    } else {
      return new String[0];
    }
  }
}
